/* global jQuery */

(function ($) {
  'use strict';

  var options = {
    zoom: {
      enableTouch: false
    },
    pan: {
      enableTouch: false,
      touchMode: 'manual'
    },
    recenter: {
      enableTouch: true
    }
  };

  var ZOOM_DISTANCE_MARGIN = $.plot.uiConstants.ZOOM_DISTANCE_MARGIN;

  function init(plot) {
    plot.hooks.processOptions.push(initTouchNavigation);
  }

  function initTouchNavigation(plot, options) {
    var gestureState = {
        zoomEnable: false,
        prevDistance: null,
        prevTapTime: 0,
        prevPanPosition: {x: 0, y: 0},
        prevTapPosition: {x: 0, y: 0}
      },
      navigationState = {
        prevTouchedAxis: 'none',
        currentTouchedAxis: 'none',
        touchedAxis: null,
        navigationConstraint: 'unconstrained',
        initialState: null
      },
      useManualPan = options.pan.interactive && options.pan.touchMode === 'manual',
      smartPanLock = options.pan.touchMode === 'smartLock',
      useSmartPan = options.pan.interactive && (smartPanLock || options.pan.touchMode === 'smart'),
      pan, pinch, doubleTap;

    function bindEvents(plot, eventHolder) {
      var o = plot.getOptions();

      if (o.zoom.interactive && o.zoom.enableTouch) {
        eventHolder[0].addEventListener('pinchstart', pinch.start, false);
        eventHolder[0].addEventListener('pinchdrag', pinch.drag, false);
        eventHolder[0].addEventListener('pinchend', pinch.end, false);
      }

      if (o.pan.interactive && o.pan.enableTouch) {
        eventHolder[0].addEventListener('panstart', pan.start, false);
        eventHolder[0].addEventListener('pandrag', pan.drag, false);
        eventHolder[0].addEventListener('panend', pan.end, false);
      }

      if ((o.recenter.interactive && o.recenter.enableTouch)) {
        eventHolder[0].addEventListener('doubletap', doubleTap.recenterPlot, false);
      }
    }

    function shutdown(plot, eventHolder) {
      eventHolder[0].removeEventListener('panstart', pan.start);
      eventHolder[0].removeEventListener('pandrag', pan.drag);
      eventHolder[0].removeEventListener('panend', pan.end);
      eventHolder[0].removeEventListener('pinchstart', pinch.start);
      eventHolder[0].removeEventListener('pinchdrag', pinch.drag);
      eventHolder[0].removeEventListener('pinchend', pinch.end);
      eventHolder[0].removeEventListener('doubletap', doubleTap.recenterPlot);
    }

    pan = {
      start: function (e) {
        presetNavigationState(e, 'pan', gestureState);
        updateData(e, 'pan', gestureState, navigationState);

        if (useSmartPan) {
          var point = getPoint(e, 'pan');
          navigationState.initialState = plot.navigationState(point.x, point.y);
        }
      },

      drag: function (e) {
        presetNavigationState(e, 'pan', gestureState);

        if (useSmartPan) {
          var point = getPoint(e, 'pan');
          plot.smartPan({
            x: navigationState.initialState.startPageX - point.x,
            y: navigationState.initialState.startPageY - point.y
          }, navigationState.initialState, navigationState.touchedAxis, false, smartPanLock);
        } else if (useManualPan) {
          plot.pan({
            left: -delta(e, 'pan', gestureState).x,
            top: -delta(e, 'pan', gestureState).y,
            axes: navigationState.touchedAxis
          });
          updatePrevPanPosition(e, 'pan', gestureState, navigationState);
        }
      },

      end: function (e) {
        presetNavigationState(e, 'pan', gestureState);

        if (useSmartPan) {
          plot.smartPan.end();
        }

        if (wasPinchEvent(e, gestureState)) {
          updateprevPanPosition(e, 'pan', gestureState, navigationState);
        }
      }
    };

    var pinchDragTimeout;
    pinch = {
      start: function (e) {
        if (pinchDragTimeout) {
          clearTimeout(pinchDragTimeout);
          pinchDragTimeout = null;
        }
        presetNavigationState(e, 'pinch', gestureState);
        setPrevDistance(e, gestureState);
        updateData(e, 'pinch', gestureState, navigationState);
      },

      drag: function (e) {
        if (pinchDragTimeout) {
          return;
        }
        pinchDragTimeout = setTimeout(function () {
          presetNavigationState(e, 'pinch', gestureState);
          plot.pan({
            left: -delta(e, 'pinch', gestureState).x,
            top: -delta(e, 'pinch', gestureState).y,
            axes: navigationState.touchedAxis
          });
          updatePrevPanPosition(e, 'pinch', gestureState, navigationState);

          var dist = pinchDistance(e);

          if (gestureState.zoomEnable || Math.abs(dist - gestureState.prevDistance) > ZOOM_DISTANCE_MARGIN) {
            zoomPlot(plot, e, gestureState, navigationState);

            //activate zoom mode
            gestureState.zoomEnable = true;
          }
          pinchDragTimeout = null;
        }, 1000 / 60);
      },

      end: function (e) {
        if (pinchDragTimeout) {
          clearTimeout(pinchDragTimeout);
          pinchDragTimeout = null;
        }
        presetNavigationState(e, 'pinch', gestureState);
        gestureState.prevDistance = null;
      }
    };

    doubleTap = {
      recenterPlot: function (e) {
        if (e && e.detail && e.detail.type === 'touchstart') {
          // only do not recenter for touch start;
          recenterPlotOnDoubleTap(plot, e, gestureState, navigationState);
        }
      }
    };

    if (options.pan.enableTouch === true || options.zoom.enableTouch === true) {
      plot.hooks.bindEvents.push(bindEvents);
      plot.hooks.shutdown.push(shutdown);
    }

    function presetNavigationState(e, gesture, gestureState) {
      navigationState.touchedAxis = getAxis(plot, e, gesture, navigationState);
      if (noAxisTouched(navigationState)) {
        navigationState.navigationConstraint = 'unconstrained';
      } else {
        navigationState.navigationConstraint = 'axisConstrained';
      }
    }
  }

  $.plot.plugins.push({
    init: init,
    options: options,
    name: 'navigateTouch',
    version: '0.3'
  });

  function recenterPlotOnDoubleTap(plot, e, gestureState, navigationState) {
    checkAxesForDoubleTap(plot, e, navigationState);
    if ((navigationState.currentTouchedAxis === 'x' && navigationState.prevTouchedAxis === 'x') ||
      (navigationState.currentTouchedAxis === 'y' && navigationState.prevTouchedAxis === 'y') ||
      (navigationState.currentTouchedAxis === 'none' && navigationState.prevTouchedAxis === 'none')) {
      var event;

      plot.recenter({axes: navigationState.touchedAxis});

      if (navigationState.touchedAxis) {
        event = new $.Event('re-center', {detail: {axisTouched: navigationState.touchedAxis}});
      } else {
        event = new $.Event('re-center', {detail: e});
      }
      plot.getPlaceholder().trigger(event);
    }
  }

  function checkAxesForDoubleTap(plot, e, navigationState) {
    var axis = plot.getTouchedAxis(e.detail.firstTouch.x, e.detail.firstTouch.y);
    if (axis[0] !== undefined) {
      navigationState.prevTouchedAxis = axis[0].direction;
    }

    axis = plot.getTouchedAxis(e.detail.secondTouch.x, e.detail.secondTouch.y);
    if (axis[0] !== undefined) {
      navigationState.touchedAxis = axis;
      navigationState.currentTouchedAxis = axis[0].direction;
    }

    if (noAxisTouched(navigationState)) {
      navigationState.touchedAxis = null;
      navigationState.prevTouchedAxis = 'none';
      navigationState.currentTouchedAxis = 'none';
    }
  }

  function zoomPlot(plot, e, gestureState, navigationState) {
    var offset = plot.offset(),
      center = {
        left: 0,
        top: 0
      },
      zoomAmount = pinchDistance(e) / gestureState.prevDistance,
      dist = pinchDistance(e);

    center.left = getPoint(e, 'pinch').x - offset.left;
    center.top = getPoint(e, 'pinch').y - offset.top;

    // send the computed touched axis to the zoom function so that it only zooms on that one
    plot.zoom({
      center: center,
      amount: zoomAmount,
      axes: navigationState.touchedAxis
    });
    gestureState.prevDistance = dist;
  }

  function wasPinchEvent(e, gestureState) {
    return (gestureState.zoomEnable && e.detail.touches.length === 1);
  }

  function getAxis(plot, e, gesture, navigationState) {
    if (e.type === 'pinchstart') {
      var axisTouch1 = plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY);
      var axisTouch2 = plot.getTouchedAxis(e.detail.touches[1].pageX, e.detail.touches[1].pageY);

      if (axisTouch1.length === axisTouch2.length && axisTouch1.toString() === axisTouch2.toString()) {
        return axisTouch1;
      }
    } else if (e.type === 'panstart') {
      return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY);
    } else if (e.type === 'pinchend') {
      //update axis since instead on pinch, a pan event is made
      return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY);
    } else {
      return navigationState.touchedAxis;
    }
  }

  function noAxisTouched(navigationState) {
    return (!navigationState.touchedAxis || navigationState.touchedAxis.length === 0);
  }

  function setPrevDistance(e, gestureState) {
    gestureState.prevDistance = pinchDistance(e);
  }

  function updateData(e, gesture, gestureState, navigationState) {
    var axisDir,
      point = getPoint(e, gesture);

    switch (navigationState.navigationConstraint) {
      case 'unconstrained':
        navigationState.touchedAxis = null;
        gestureState.prevTapPosition = {
          x: gestureState.prevPanPosition.x,
          y: gestureState.prevPanPosition.y
        };
        gestureState.prevPanPosition = {
          x: point.x,
          y: point.y
        };
        break;
      case 'axisConstrained':
        axisDir = navigationState.touchedAxis[0].direction;
        navigationState.currentTouchedAxis = axisDir;
        gestureState.prevTapPosition[axisDir] = gestureState.prevPanPosition[axisDir];
        gestureState.prevPanPosition[axisDir] = point[axisDir];
        break;
      default:
        break;
    }
  }

  function distance(x1, y1, x2, y2) {
    return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
  }

  function pinchDistance(e) {
    var t1 = e.detail.touches[0],
      t2 = e.detail.touches[1];
    return distance(t1.pageX, t1.pageY, t2.pageX, t2.pageY);
  }

  function updatePrevPanPosition(e, gesture, gestureState, navigationState) {
    var point = getPoint(e, gesture);

    switch (navigationState.navigationConstraint) {
      case 'unconstrained':
        gestureState.prevPanPosition.x = point.x;
        gestureState.prevPanPosition.y = point.y;
        break;
      case 'axisConstrained':
        gestureState.prevPanPosition[navigationState.currentTouchedAxis] =
          point[navigationState.currentTouchedAxis];
        break;
      default:
        break;
    }
  }

  function delta(e, gesture, gestureState) {
    var point = getPoint(e, gesture);

    return {
      x: point.x - gestureState.prevPanPosition.x,
      y: point.y - gestureState.prevPanPosition.y
    }
  }

  function getPoint(e, gesture) {
    if (gesture === 'pinch') {
      return {
        x: (e.detail.touches[0].pageX + e.detail.touches[1].pageX) / 2,
        y: (e.detail.touches[0].pageY + e.detail.touches[1].pageY) / 2
      }
    } else {
      return {
        x: e.detail.touches[0].pageX,
        y: e.detail.touches[0].pageY
      }
    }
  }
})(jQuery);
